/**
 * 
 */
package net.phoenix.repository.query.xpath;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.jcr.Node;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.ValueFormatException;
import javax.jcr.nodetype.ItemDefinition;
import javax.jcr.nodetype.NodeDefinition;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.query.Query;

import net.phoenix.repository.JCRUtils;
import net.phoenix.repository.dao.QDBName;

import org.apache.xml.dtm.Axis;
import org.jaxen.JaxenException;
import org.jaxen.saxpath.Operator;
import org.jaxen.saxpath.XPathHandler;

/**
 * @author lixf
 * 
 */
public class SQLGeneratorHandler implements XPathHandler {
	private SQLExpr sql;
	private Node parent;
	private ValueFactory valueFactory;
	private Session session;

	/**
	 * This may be changed to an ArrayList in the future (i.e. version &gt;=
	 * 1.2). You really shouldn't be accessing this field directly, but if you
	 * are please try to use it as a generic List. Don't use the methods that
	 * are only available in LinkedList.
	 */
	protected LinkedList<LinkedList<SQLExpr>> stack;

	/**
	 * Constructor
	 * 
	 * @throws RepositoryException
	 */
	public SQLGeneratorHandler(Node parent) throws RepositoryException {
		this.parent = parent;
		this.session = parent.getSession();
		this.valueFactory = this.session.getValueFactory();
		this.stack = new LinkedList<LinkedList<SQLExpr>>();
	}

	public Query toQuery() throws RepositoryException {
		Query query = this.parent.getSession().getWorkspace().getQueryManager().createQuery(this.sql.getSQL(), Query.JCR_SQL2);
		List<Value> parameters = this.sql.getParameters();
		for (int i = 0; i < parameters.size(); i++) {
			query.bindValue("" + i, parameters.get(i));
		}
		return query;
	}

	/**
	 * Retrieve the simplified Jaxen XPath expression tree.
	 * 
	 * <p>
	 * This method is only valid once <code>XPathReader.parse(...)</code>
	 * successfully returned.
	 * </p>
	 * 
	 * @return the XPath expression tree
	 */
	public String getSQL() {
		return this.sql.getSQL();
	}

	public List<Value> getParameters() {
		return this.sql.getParameters();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jaxen.saxpath.XPathHandler#startXPath()
	 */
	public void startXPath() {
		pushFrame();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jaxen.saxpath.XPathHandler#endXPath()
	 */
	public void endXPath() throws JaxenException {
		this.sql = pop();
		popFrame();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jaxen.saxpath.XPathHandler#startPathExpr()
	 */
	public void startPathExpr() {
		pushFrame();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jaxen.saxpath.XPathHandler#endPathExpr()
	 */
	public void endPathExpr() throws JaxenException {

		// PathExpr ::= LocationPath
		// | FilterExpr
		// | FilterExpr / RelativeLocationPath
		// | FilterExpr // RelativeLocationPath
		//
		// If the current stack-frame has two items, it's a
		// FilterExpr and a LocationPath (of some flavor).
		//
		// If the current stack-frame has one item, it's simply
		// a FilterExpr, and more than likely boils down to a
		// primary expr of some flavor. But that's for another
		// method...

		SQLExpr filterExpr;
		SQLExpr locationPath;

		SQLExpr popped;

		if (stackSize() == 2) {
			locationPath = pop();
			filterExpr = pop();
			popFrame();
			push(locationPath.path(filterExpr));
		} else {
			popped = pop();
			popFrame();
			push(popped);
		}

	}

	public void startAbsoluteLocationPath() throws JaxenException {
		pushFrame();
		try {
			SQLExpr expr = new SQLExpr(this.parent.getDefinition());
			push(expr);
		} catch (RepositoryException e) {
			throw new JaxenException(e);
		}
	}

	public void endAbsoluteLocationPath() throws JaxenException {
		endLocationPath();
	}

	public void startRelativeLocationPath() throws JaxenException {
		ItemDefinition type = this.peekItemDefinition();
		try {
			if (type != null) {
				SQLExpr expr = new SQLExpr(type);
				pushFrame();
				push(expr);
			} else {
				SQLExpr expr = new SQLExpr(parent.getDefinition());
				expr.where = " node.parentId=? ";
				expr.parameters.add(this.valueFactory.createValue(parent.getIdentifier()));
				pushFrame();
				push(expr);

			}
		} catch (RepositoryException ex) {
			throw new JaxenException(ex);
		}

	}

	public void endRelativeLocationPath() throws JaxenException {
		endLocationPath();
	}

	protected void endLocationPath() throws JaxenException {
		SQLExpr path = peekFrame().removeFirst();

		addSteps(path, popFrame().iterator());

		push(path);
	}

	protected void addSteps(SQLExpr locationPath, Iterator<SQLExpr> stepIter) {
		while (stepIter.hasNext()) {
			locationPath.addStep(stepIter.next());
		}
	}

	public void startNameStep(int axis, String prefix, String localName) throws JaxenException {
		switch (axis) {
		case Axis.ANCESTORORSELF:
			this.processChildStep(prefix, localName);
			break;
		case Axis.NAMESPACE:
		case Axis.ATTRIBUTE:
			this.processAttributeStep(prefix, localName);
			break;
		default:
			System.err.println("Unknown axis" + axis + " for name:" + localName);
		}
	}

	private void processChildStep(String prefix, String localName) throws JaxenException {
		SQLExpr parent = this.peekFrame().getFirst();
		NodeDefinition nodeDefinition;
		try {
			if (parent.definition == null)
				nodeDefinition = JCRUtils.getChildNodeDefinition(this.parent.getPrimaryNodeType(), localName);
			else
				nodeDefinition = JCRUtils.getChildNodeDefinition((NodeDefinition) parent.definition, localName);
		} catch (RepositoryException ex) {
			throw new JaxenException(ex);
		}
		SQLExpr expr = new SQLExpr(nodeDefinition);
		expr.where = localName;
		pushFrame();
		push(expr);

	}

	private void processAttributeStep(String prefix, String localName) throws JaxenException {
		NodeDefinition nodeDefinition = this.peekNodeDefinition();
		PropertyDefinition propertyDefinition = JCRUtils.getPropertyDefinition(nodeDefinition, localName);
		SQLExpr expr = new SQLExpr(propertyDefinition);
		expr.where = localName;
		pushFrame();
		push(expr);
	}

	public void endNameStep() {
		endStep();
	}

	public void startTextNodeStep(int axis) throws JaxenException {
		// System.err.println("startTextNodeStep()");
		pushFrame();
		SQLExpr expr = new SQLExpr(this.peekItemDefinition());
		push(expr);
	}

	public void endTextNodeStep() {
		endStep();
	}

	public void startCommentNodeStep(int axis) throws JaxenException {
		pushFrame();
		SQLExpr expr = new SQLExpr(this.peekItemDefinition());
		push(expr);
	}

	public void endCommentNodeStep() {
		endStep();
	}

	public void startAllNodeStep(int axis) throws JaxenException {
		pushFrame();
		SQLExpr expr = new SQLExpr(this.peekItemDefinition());
		push(expr);
	}

	public void endAllNodeStep() {
		endStep();
	}

	public void startProcessingInstructionNodeStep(int axis, String name) throws JaxenException {
		pushFrame();
		SQLExpr expr = new SQLExpr(name);
		push(expr);
	}

	public void endProcessingInstructionNodeStep() {
		endStep();
	}

	protected void endStep() {
		SQLExpr step = (SQLExpr) peekFrame().removeFirst();

		addPredicates(step, popFrame().iterator());

		push(step);
	}

	public void startPredicate() {
		pushFrame();
	}

	public void endPredicate() throws JaxenException {
		SQLExpr predicate = pop().predicate();
		popFrame();
		push(predicate);
	}

	public void startFilterExpr() {
		pushFrame();
	}

	public void endFilterExpr() throws JaxenException {
		SQLExpr expr = peekFrame().removeFirst();

		SQLExpr filter = expr.filter();

		Iterator<SQLExpr> predIter = popFrame().iterator();

		addPredicates(filter, predIter);

		push(filter);
	}

	protected void addPredicates(SQLExpr obj, Iterator<SQLExpr> predIter) {
		while (predIter.hasNext()) {
			obj.addPredicate(predIter.next());
		}
	}

	protected void returnExpr() {
		SQLExpr expr = pop();
		popFrame();
		push(expr);
	}

	public void startOrExpr() {
	}

	public void endOrExpr(boolean create) throws JaxenException {

		if (create) {
			SQLExpr rhs = (SQLExpr) pop();
			SQLExpr lhs = (SQLExpr) pop();
			push(lhs.or(rhs));
		}
	}

	public void startAndExpr() {
	}

	public void endAndExpr(boolean create) throws JaxenException {

		if (create) {

			SQLExpr rhs = pop();
			SQLExpr lhs = pop();

			push(lhs.and(rhs));
		}
	}

	public void startEqualityExpr() {
	}

	public void endEqualityExpr(int operator) throws JaxenException {

		if (operator != Operator.NO_OP) {

			SQLExpr rhs = pop();
			SQLExpr lhs = pop();

			push(lhs.equality(rhs, operator));
		}
	}

	public void startRelationalExpr() {
	}

	public void endRelationalExpr(int operator) throws JaxenException {

		if (operator != Operator.NO_OP) {

			SQLExpr rhs = pop();
			SQLExpr lhs = pop();

			push(lhs.relational(rhs, operator));
		}
	}

	public void startAdditiveExpr() {
	}

	public void endAdditiveExpr(int operator) throws JaxenException {

		if (operator != Operator.NO_OP) {

			SQLExpr rhs = (SQLExpr) pop();
			SQLExpr lhs = (SQLExpr) pop();

			push(lhs.additive(rhs, operator));
		}
	}

	public void startMultiplicativeExpr() {
	}

	public void endMultiplicativeExpr(int operator) throws JaxenException {

		if (operator != Operator.NO_OP) {

			SQLExpr rhs = (SQLExpr) pop();
			SQLExpr lhs = (SQLExpr) pop();

			push(lhs.multiplicative(rhs, operator));
		}
	}

	public void startUnaryExpr() {
	}

	public void endUnaryExpr(int operator) throws JaxenException {

		if (operator != Operator.NO_OP) {
			push(pop().unary(operator));
		}
	}

	public void startUnionExpr() {
	}

	public void endUnionExpr(boolean create) throws JaxenException {

		if (create) {

			SQLExpr rhs = (SQLExpr) pop();
			SQLExpr lhs = (SQLExpr) pop();

			push(lhs.union(rhs));
		}
	}

	public void number(int number) throws JaxenException {
		SQLExpr expr = new SQLExpr(number);
		push(expr);

	}

	public void number(double number) throws JaxenException {
		SQLExpr expr = new SQLExpr(number);
		push(expr);

	}

	public void literal(String literal) throws JaxenException {
		SQLExpr expr = new SQLExpr(literal);
		push(expr);

	}

	public void variableReference(String prefix, String variableName) throws JaxenException {
		SQLExpr expr = new SQLExpr(variableName);
		push(expr);

	}

	public void startFunction(String prefix, String functionName) throws JaxenException {
		pushFrame();

		SQLExpr expr = new SQLExpr(this.peekItemDefinition());
		expr.where = functionName + "()";

		push(expr);
	}

	public void endFunction() {
		SQLExpr function = peekFrame().removeFirst();

		addParameters(function, popFrame().iterator());

		push(function);
	}

	private void addParameters(SQLExpr function, Iterator<SQLExpr> paramIter) {
		while (paramIter.hasNext()) {
			function.addParameter((SQLExpr) paramIter.next());
		}
	}

	protected int stackSize() {
		return peekFrame().size();
	}

	protected void push(SQLExpr obj) {
		peekFrame().addLast(obj);
		debug();
	}

	protected SQLExpr pop() {
		debug();
		return peekFrame().removeLast();
	}

	protected boolean canPop() {
		return (peekFrame().size() > 0);
	}

	protected void pushFrame() {
		this.stack.addLast(new LinkedList<SQLExpr>());
	}

	protected LinkedList<SQLExpr> popFrame() {
		return (LinkedList<SQLExpr>) this.stack.removeLast();
	}

	protected LinkedList<SQLExpr> peekFrame() {
		return (LinkedList<SQLExpr>) this.stack.getLast();
	}

	/**
	 * @return
	 */
	protected ItemDefinition peekItemDefinition() throws JaxenException {
		for (Iterator<LinkedList<SQLExpr>> frames = this.stack.descendingIterator(); frames.hasNext();) {
			LinkedList<SQLExpr> frame = frames.next();
			for (Iterator<SQLExpr> expres = frame.descendingIterator(); expres.hasNext();) {
				SQLExpr expre = expres.next();
				if (expre.definition != null)
					return expre.definition;
			}
		}
		return null;
	}

	protected NodeDefinition peekNodeDefinition() throws JaxenException {
		for (Iterator<LinkedList<SQLExpr>> frames = this.stack.descendingIterator(); frames.hasNext();) {
			LinkedList<SQLExpr> frame = frames.next();
			for (Iterator<SQLExpr> expres = frame.descendingIterator(); expres.hasNext();) {
				SQLExpr expre = expres.next();
				if (expre.definition != null && expre.definition instanceof NodeDefinition)
					return (NodeDefinition) expre.definition;
			}
		}
		return null;
	}

	private void debug() {
		System.err.println("Stack Info:" + this.stack);
		// for (LinkedList<SQLExpr> frame : this.stack) {
		// System.err.print("Frame Begin<<");
		// for (SQLExpr expr : frame)
		// System.err.print("--" + expr.toString());
		// System.err.println(">>Frame End");
		// }
	}

	// private String table(String propertyName) {
	// PropertyDefinition property = JCRUtils.getPropertyDefinition(nodeType,
	// propertyName);
	// if (property != null)
	// return QName.encode(property.getRequiredType(), propertyName);
	// throw new IllegalArgumentException("Unknown property for " + propertyName
	// + " in type " + nodeType.getName());
	// }
	class SQLExpr {
		private String where;
		private String order;
		private Set<String> tables;
		private List<Value> parameters;
		private ItemDefinition definition;
		private boolean constant;

		public SQLExpr(ItemDefinition type) {
			this.parameters = new ArrayList<Value>();
			this.tables = new LinkedHashSet<String>();
			this.definition = type;
			this.constant = false;
		}

		public SQLExpr(String constant) {
			this.parameters = new ArrayList<Value>();
			this.tables = new LinkedHashSet<String>();
			this.definition = null;
			this.constant = true;
			this.where = constant;
		}

		public SQLExpr(int constant) {
			this.parameters = new ArrayList<Value>();
			this.tables = new LinkedHashSet<String>();
			this.definition = null;
			this.constant = true;
			this.where = Integer.toString(constant);
		}

		public SQLExpr(double constant) {
			this.parameters = new ArrayList<Value>();
			this.tables = new LinkedHashSet<String>();
			this.definition = null;
			this.constant = true;
			this.where = Double.toString(constant);
		}

		public SQLExpr predicate() throws JaxenException {
			if (this.constant) {
				Value value;
				try {
					value = valueFactory.createValue(this.where, PropertyType.LONG);
				} catch (ValueFormatException e) {
					throw new JaxenException(e);
				}
				this.where = " node.orderNo=?";
				this.parameters.add(value);
			}
			return this;
		}

		public SQLExpr filter() {
			return this;
		}

		public void addParameter(Value param) {
			this.parameters.add(param);
		}

		public String getSQL() {
			StringBuffer buffer = new StringBuffer();
			buffer.append("SELECT * FROM node ");
			for (String table : this.tables)
				buffer.append("," + table);
			buffer.append(" WHERE ");
			if (this.where != null)
				buffer.append(this.where);
			buffer.append(" ORDER BY");
			if (this.order != null)
				buffer.append(this.order);
			buffer.append(" node.orderNO ");
			return buffer.toString();
		}

		public void addStep(SQLExpr step) {
			if (this.tables.size() == 0) {
				this.order = step.order;
				this.parameters.addAll(step.parameters);
				this.tables.addAll(step.tables);
				if (this.where != null)
					this.where = this.where + " AND " + step.where;
				else
					this.where = step.where;

				this.definition = step.definition;
			} else {
				this.tables.add("(" + step.getSQL() + ") parent");
				this.where = "node.parentId=parent.pkid AND " + this.where;
				this.parameters.addAll(0, step.parameters);
			}
		}

		public void addPredicate(SQLExpr next) {

			if (this.where.length() > 0) {
				if (!this.where.contains(" ")) {
					this.parameters.add(valueFactory.createValue(this.where));
					this.where = "node.name=?";
				}
				this.where = this.where + " AND ";
			}
			this.where = this.where + next.where;
			this.parameters.addAll(next.parameters);
			this.tables.addAll(next.tables);
		}

		public void addParameter(SQLExpr next) {
			if (this.where.endsWith("()"))
				this.where = this.where.substring(0, this.where.length() - 1) + next.where + ")";
			else
				this.where = this.where.substring(0, this.where.length() - 1) + "," + next.where + ")";
			this.parameters.addAll(next.parameters);
			this.tables.addAll(next.tables);
		}

		public SQLExpr or(SQLExpr right) {
			this.where = "(" + this.where + ") OR (" + right.where + ")";
			this.parameters.addAll(right.parameters);
			this.tables.addAll(right.tables);
			return this;
		}

		public SQLExpr and(SQLExpr right) {
			this.where = "(" + this.where + ") AND (" + right.where + ")";
			this.parameters.addAll(right.parameters);
			this.tables.addAll(right.tables);
			return this;
		}

		/**
		 * 当前的where中保存属性名称，definition中保存属性类型；
		 * 
		 * @param rhs
		 * @param operator
		 * @return
		 * @throws JaxenException
		 */
		public SQLExpr equality(SQLExpr rhs, int operator) throws JaxenException {
			int type = ((PropertyDefinition) definition).getRequiredType();
			Value value;
			try {
				value = valueFactory.createValue(rhs.where, type);
			} catch (ValueFormatException e) {
				throw new JaxenException(e);
			}
			String table = QDBName.encode(type, this.where);
			StringBuffer buffer = new StringBuffer("(");
			buffer.append(table).append(".node_id=node.pkid AND ").append(table).append(".value");
			switch (operator) {
			case Operator.EQUALS:
				buffer.append("=");
				break;
			case Operator.NOT_EQUALS:
				buffer.append("<>");
				break;
			}
			buffer.append("?)");
			this.where = buffer.toString();
			this.tables.add(table);
			this.tables.addAll(rhs.tables);
			this.parameters.addAll(rhs.parameters);
			this.parameters.add(value);
			return this;
		}

		public SQLExpr additive(SQLExpr rhs, int operator) {
			switch (operator) {
			case Operator.ADD:
				this.where = this.where + "+" + rhs.where;
				break;
			case Operator.SUBTRACT:
				this.where = this.where + "-" + rhs.where;
				break;
			}
			this.tables.addAll(rhs.tables);
			this.parameters.addAll(rhs.parameters);
			return this;
		}

		public SQLExpr multiplicative(SQLExpr rhs, int operator) {
			switch (operator) {
			case Operator.MULTIPLY:
				this.where = this.where + "*" + rhs.where;
				break;
			case Operator.DIV:
				this.where = this.where + "/" + rhs.where;
				break;
			case Operator.MOD:
				this.where = this.where + " mod " + rhs.where;
				break;
			}
			this.tables.addAll(rhs.tables);
			this.parameters.addAll(rhs.parameters);
			return this;
		}

		public SQLExpr relational(SQLExpr rhs, int operator) throws JaxenException {
			int type = ((PropertyDefinition) definition).getRequiredType();
			Value value;
			try {
				value = valueFactory.createValue(rhs.where, type);
			} catch (ValueFormatException e) {
				throw new JaxenException(e);
			}
			String table = QDBName.encode(type, this.where);
			StringBuffer buffer = new StringBuffer("(");
			buffer.append(table).append(".node_id=node.pkid AND ").append(table).append(".value");
			switch (operator) {
			case Operator.GREATER_THAN:
				buffer.append(">");
				break;
			case Operator.GREATER_THAN_EQUALS:
				buffer.append(">=");
				break;
			case Operator.LESS_THAN:
				buffer.append("<");
				break;
			case Operator.LESS_THAN_EQUALS:
				buffer.append("<=");
				break;
			case Operator.NOT_EQUALS:
				buffer.append("<>");
				break;
			case Operator.EQUALS:
				buffer.append("=");
				break;
			}
			buffer.append("?)");
			this.where = buffer.toString();
			this.tables.add(table);
			this.tables.addAll(rhs.tables);
			this.parameters.addAll(rhs.parameters);
			this.parameters.add(value);
			return this;

		}

		public SQLExpr unary(int operator) {
			return this;
		}

		public SQLExpr union(SQLExpr rhs) {
			return this;
		}

		public SQLExpr path(SQLExpr filterExpr) {
			if (filterExpr != null)
				return filterExpr;
			return this;

		}

		public List<Value> getParameters() {
			return this.parameters;
		}

		public String toString() {
			StringBuffer buffer = new StringBuffer();
			buffer.append("(");
			if (this.definition != null) {
				buffer.append(this.definition.getName());
				buffer.append(",");
			}
			buffer.append(this.where);
			buffer.append(")");
			return buffer.toString();

		}
	}

}
